/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.geode.internal.jndi;

import org.apache.geode.distributed.DistributedSystem;
import org.apache.geode.distributed.internal.DistributionConfig;
import org.apache.geode.i18n.LogWriterI18n;
import org.apache.geode.internal.ClassPathLoader;
import org.apache.geode.internal.datasource.AbstractDataSource;
import org.apache.geode.internal.datasource.ClientConnectionFactoryWrapper;
import org.apache.geode.internal.datasource.DataSourceCreateException;
import org.apache.geode.internal.datasource.DataSourceFactory;
import org.apache.geode.internal.i18n.LocalizedStrings;
import org.apache.geode.internal.jta.TransactionManagerImpl;
import org.apache.geode.internal.jta.TransactionUtils;
import org.apache.geode.internal.jta.UserTransactionImpl;

import javax.naming.*;
import javax.transaction.SystemException;
import javax.transaction.TransactionManager;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

/**
 * <p>
 * Utility class for binding DataSources and Transactional resources to JNDI
 * Tree. If there is a pre-existing JNDI tree in the system, the GemFire
 * JNDI tree is not not generated.
 * </p><p>
 * Datasources are bound to jndi tree after getting initialised. The
 * initialisation parameters are read from cache.xml.
 * </p><p>
 * If there is a pre-existing TransactionManager/UserTransaction (possible
 * in presence of application server scenerio), the GemFire TransactionManager
 * and UserTransaction will not be initialised. In that case, application server
 * TransactionManager/UserTransaction will handle the transactional activity. But
 * even in this case the datasource element will be bound to the available JNDI
 * tree. The transactional datasource (XADataSource) will make use of available
 * TransactionManager.
 * </p>
 * 
 */
public class JNDIInvoker  {

//  private static boolean DEBUG = false;
  /**
   * JNDI Context, this may refer to GemFire JNDI Context or external Context,
   * in case the external JNDI tree exists.
   */
  private static Context ctx;
  /**
   * transactionManager TransactionManager, this refers to GemFire
   * TransactionManager only.
   */
  private static TransactionManager transactionManager;
  // most of the following came from the javadocs at:
  // http://static.springsource.org/spring/docs/2.5.x/api/org/springframework/transaction/jta/JtaTransactionManager.html
  private static String[][] knownJNDIManagers = {
      {"java:/TransactionManager", "JBoss"},
      {"java:comp/TransactionManager","Cosminexus"}, // and many others
      {"java:appserver/TransactionManager","GlassFish"},
      {"java:pm/TransactionManager","SunONE"},
      {"java:comp/UserTransaction","Orion, JTOM, BEA WebLogic"},
      // not sure about the following but leaving it for backwards compat
      {"javax.transaction.TransactionManager", "BEA WebLogic"}
   };
  /** ************************************************* */
  /**
   * WebSphere 5.1 TransactionManagerFactory
   */
  private static final String WS_FACTORY_CLASS_5_1 = "com.ibm.ws.Transaction.TransactionManagerFactory";
  /**
   * WebSphere 5.0 TransactionManagerFactory
   */
  private static final String WS_FACTORY_CLASS_5_0 = "com.ibm.ejs.jts.jta.TransactionManagerFactory";
  /**
   * WebSphere 4.0 TransactionManagerFactory
   */
  private static final String WS_FACTORY_CLASS_4 = "com.ibm.ejs.jts.jta.JTSXA";
  /**
   * List of DataSource bound to the context, used for cleaning gracefully
   * closing datasource and associated threads.
   */
  private static List dataSourceList = new ArrayList();

  /**
   * If this system property is set to true, GemFire will not try to lookup for an
   * existing JTA transaction manager bound to JNDI context or try to bind itself
   * as a JTA transaction manager. Also region operations will <b>not</b> participate in
   * an ongoing JTA transaction. 
   */
  private static Boolean IGNORE_JTA = Boolean.getBoolean(DistributionConfig.GEMFIRE_PREFIX + "ignoreJTA");

  /**
   * Bind the transaction resources. Bind UserTransaction and
   * TransactionManager. 
   * <p>
   * If there is pre-existing JNDI tree in the system
   * and TransactionManager / UserTransaction is already bound, GemFire will
   * make use of these resources, but if TransactionManager / UserTransaction is
   * not available, the GemFire TransactionManager / UserTransaction will be
   * bound to the JNDI tree.
   * </p>
   *  
   */
  public static void mapTransactions(DistributedSystem distSystem) {
    try {
      TransactionUtils.setLogWriter(distSystem.getLogWriter().convertToLogWriterI18n());
      cleanup();
      if (IGNORE_JTA) {
        return;
      }
      ctx = new InitialContext();
      doTransactionLookup();
    }
    catch (NamingException ne) {
      LogWriterI18n writer = TransactionUtils.getLogWriterI18n();
      if (ne instanceof NoInitialContextException) {
        String exception = "JNDIInvoker::mapTransactions:: No application server context found, Starting GemFire JNDI Context Context ";
        if (writer.finerEnabled()) writer.finer(exception);
        try {
          initializeGemFireContext();
          transactionManager = TransactionManagerImpl.getTransactionManager();
          ctx.rebind("java:/TransactionManager", transactionManager);
          if (writer.fineEnabled())
              writer
                  .fine("JNDIInvoker::mapTransactions::Bound TransactionManager to Context GemFire JNDI Tree");
          UserTransactionImpl utx = new UserTransactionImpl();
          ctx.rebind("java:/UserTransaction", utx);
          if (writer.fineEnabled())
              writer
                  .fine("JNDIInvoker::mapTransactions::Bound Transaction to Context GemFire JNDI Tree");
        }
        catch (NamingException ne1) {
          if (writer.infoEnabled())
              writer
                  .info(LocalizedStrings.JNDIInvoker_JNDIINVOKERMAPTRANSACTIONSNAMINGEXCEPTION_WHILE_BINDING_TRANSACTIONMANAGERUSERTRANSACTION_TO_GEMFIRE_JNDI_TREE);
        }
        catch (SystemException se1) {
          if (writer.infoEnabled())
              writer
                  .info(LocalizedStrings.JNDIInvoker_JNDIINVOKERMAPTRANSACTIONSSYSTEMEXCEPTION_WHILE_BINDING_USERTRANSACTION_TO_GEMFIRE_JNDI_TREE);
        }
      }
      else if (ne instanceof NameNotFoundException) {
        String exception = "JNDIInvoker::mapTransactions:: No TransactionManager associated to Application server context, trying to bind GemFire TransactionManager";
        if (writer.finerEnabled()) writer.finer(exception);
        try {
          transactionManager = TransactionManagerImpl.getTransactionManager();
          ctx.rebind("java:/TransactionManager", transactionManager);
          if (writer.fineEnabled())
              writer
                  .fine("JNDIInvoker::mapTransactions::Bound TransactionManager to Application Server Context");
          UserTransactionImpl utx = new UserTransactionImpl();
          ctx.rebind("java:/UserTransaction", utx);
          if (writer.fineEnabled())
              writer
                  .fine("JNDIInvoker::mapTransactions::Bound UserTransaction to Application Server Context");
        }
        catch (NamingException ne1) {
          if (writer.infoEnabled())
              writer
                  .info(LocalizedStrings.JNDIInvoker_JNDIINVOKERMAPTRANSACTIONSNAMINGEXCEPTION_WHILE_BINDING_TRANSACTIONMANAGERUSERTRANSACTION_TO_APPLICATION_SERVER_JNDI_TREE);
        }
        catch (SystemException se1) {
          if (writer.infoEnabled())
              writer
                  .info(LocalizedStrings.JNDIInvoker_JNDIINVOKERMAPTRANSACTIONSSYSTEMEXCEPTION_WHILE_BINDING_TRANSACTIONMANAGERUSERTRANSACTION_TO_APPLICATION_SERVER_JNDI_TREE);
        }
      }
    }
  }

  /*
   * Cleans all the DataSource ans its associated threads gracefully, before
   * start of the GemFire system. Also kills all of the threads associated with
   * the TransactionManager before making it null.
   */
  private static void cleanup() {
    if (transactionManager instanceof TransactionManagerImpl) {
      TransactionManagerImpl.refresh();
      // unbind and set TransactionManager to null, so as to
      // initialize TXManager correctly if the cache is being restarted
      transactionManager = null;
      try {
        if (ctx != null) {
          ctx.unbind("java:/TransactionManager");
        }
      } catch (NamingException e) {
        // ok to ignore, rebind will be tried later
      }
    }
    int len = dataSourceList.size();
    for (int i = 0; i < len; i++) {
      if (dataSourceList.get(i) instanceof AbstractDataSource)
        ((AbstractDataSource) dataSourceList.get(i)).clearUp();
      else if (dataSourceList.get(i) instanceof ClientConnectionFactoryWrapper) {
        ((ClientConnectionFactoryWrapper) dataSourceList.get(i)).clearUp();
      }
    }
    dataSourceList.clear();
    IGNORE_JTA = Boolean.getBoolean(DistributionConfig.GEMFIRE_PREFIX + "ignoreJTA");
  }

  /*
   * Helps in locating TransactionManager in presence of application server
   * scenario. Stores the value of TransactionManager for reference in GemFire
   * system.
   */
  private static void doTransactionLookup() throws NamingException {
    Object jndiObject = null;
    LogWriterI18n writer = TransactionUtils.getLogWriterI18n();
    for (int i = 0; i < knownJNDIManagers.length; i++) {
      try {
        jndiObject = ctx.lookup(knownJNDIManagers[i][0]);
      }
      catch (NamingException e) {
        String exception = "JNDIInvoker::doTransactionLookup::Couldn't lookup ["
            + knownJNDIManagers[i][0] + " (" + knownJNDIManagers[i][1] + ")]";
        if (writer.finerEnabled()) writer.finer(exception);
      }
      if (jndiObject instanceof TransactionManager) {
        transactionManager = (TransactionManager) jndiObject;
        String exception = "JNDIInvoker::doTransactionLookup::Found TransactionManager for "
            + knownJNDIManagers[i][1];
        if (writer.fineEnabled()) writer.fine(exception);
        return;
      } else {
        String exception = "JNDIInvoker::doTransactionLookup::Found TransactionManager of class "
          + (jndiObject == null?"null":jndiObject.getClass()) + " but is not of type javax.transaction.TransactionManager" ;
        if (writer.fineEnabled()) writer.fine(exception);
      }
    }
    Class clazz;
    try {
      if (writer.finerEnabled())
          writer
              .finer("JNDIInvoker::doTransactionLookup::Trying WebSphere 5.1: "
                  + WS_FACTORY_CLASS_5_1);
      clazz = ClassPathLoader.getLatest().forName(WS_FACTORY_CLASS_5_1);
      if (writer.fineEnabled())
          writer.fine("JNDIInvoker::doTransactionLookup::Found WebSphere 5.1: "
              + WS_FACTORY_CLASS_5_1);
    }
    catch (ClassNotFoundException ex) {
      try {
        if (writer.finerEnabled())
            writer
                .finer("JNDIInvoker::doTransactionLookup::Trying WebSphere 5.0: "
                    + WS_FACTORY_CLASS_5_0);
        clazz = ClassPathLoader.getLatest().forName(WS_FACTORY_CLASS_5_0);
        if (writer.fineEnabled())
            writer
                .fine("JNDIInvoker::doTransactionLookup::Found WebSphere 5.0: "
                    + WS_FACTORY_CLASS_5_0);
      }
      catch (ClassNotFoundException ex2) {
        try {
          clazz = ClassPathLoader.getLatest().forName(WS_FACTORY_CLASS_4);
          String exception = "JNDIInvoker::doTransactionLookup::Found WebSphere 4: "
              + WS_FACTORY_CLASS_4;
          if (writer.fineEnabled()) writer.fine(exception, ex);
        }
        catch (ClassNotFoundException ex3) {
          if (writer.finerEnabled())
              writer
                  .finer("JNDIInvoker::doTransactionLookup::Couldn't find any WebSphere TransactionManager factory class, neither for WebSphere version 5.1 nor 5.0 nor 4");
          throw new NoInitialContextException();
        }
      }
    }
    try {
      Method method = clazz.getMethod("getTransactionManager", (Class[])null);
      transactionManager = (TransactionManager) method.invoke(null, (Object[])null);
    }
    catch (Exception ex) {
      writer.warning(LocalizedStrings.JNDIInvoker_JNDIINVOKER_DOTRANSACTIONLOOKUP_FOUND_WEBSPHERE_TRANSACTIONMANAGER_FACTORY_CLASS_0_BUT_COULDNT_INVOKE_ITS_STATIC_GETTRANSACTIONMANAGER_METHOD, clazz.getName(), ex);
      throw new NameNotFoundException(LocalizedStrings.JNDIInvoker_JNDIINVOKER_DOTRANSACTIONLOOKUP_FOUND_WEBSPHERE_TRANSACTIONMANAGER_FACTORY_CLASS_0_BUT_COULDNT_INVOKE_ITS_STATIC_GETTRANSACTIONMANAGER_METHOD.toLocalizedString(new Object[] {clazz.getName()}));
    }
  }

  /**
   * Initialises the GemFire context. This is called when no external JNDI
   * Context is found. 
   * 
   * @throws NamingException
   */
  private static void initializeGemFireContext() throws NamingException {
    Hashtable table = new Hashtable();
    table.put(Context.INITIAL_CONTEXT_FACTORY,
        "org.apache.geode.internal.jndi.InitialContextFactoryImpl");
    ctx = new InitialContext(table);
  }

  /**
   * Binds a single Datasource to the existing JNDI tree. The JNDI tree may be
   * The Datasource properties are contained in the map. The Datasource
   * implementation class is populated based on properties in the map.
   * 
   * @param map contains Datasource configuration properties.
   */
  public static void mapDatasource(Map map, List props) {
    String value = (String) map.get("type");
    String jndiName = "";
    LogWriterI18n writer = TransactionUtils.getLogWriterI18n();
    Object ds = null;
    try {
      jndiName = (String) map.get("jndi-name");
      if (value.equals("PooledDataSource")) {
        ds = DataSourceFactory.getPooledDataSource(map, props);
        ctx.rebind("java:/" + jndiName, ds);
        dataSourceList.add(ds);
        if (writer.fineEnabled())
            writer.fine("Bound java:/" + jndiName + " to Context");
      }
      else if (value.equals("XAPooledDataSource")) {
        ds = DataSourceFactory.getTranxDataSource(map, props);
        ctx.rebind("java:/" + jndiName, ds);
        dataSourceList.add(ds);
        if (writer.fineEnabled())
            writer.fine("Bound java:/" + jndiName + " to Context");
      }
      else if (value.equals("SimpleDataSource")) {
        ds = DataSourceFactory.getSimpleDataSource(map, props);
        ctx.rebind("java:/" + jndiName, ds);
        if (writer.fineEnabled())
            writer.fine("Bound java:/" + jndiName + " to Context");
      }
      else if (value.equals("ManagedDataSource")) {
        ClientConnectionFactoryWrapper ds1 = DataSourceFactory
            .getManagedDataSource(map, props);
        ctx.rebind("java:/" + jndiName, ds1.getClientConnFactory());
        dataSourceList.add(ds1);
        if (writer.fineEnabled())
            writer.fine("Bound java:/" + jndiName + " to Context");
      }
      else {
        String exception = "JNDIInvoker::mapDataSource::No correct type of DataSource";
        if (writer.fineEnabled()) writer.fine(exception);
        throw new DataSourceCreateException(exception);
      }
      ds = null;
    }
    catch (NamingException ne) {
      if (writer.infoEnabled()) writer.info(
          LocalizedStrings.JNDIInvoker_JNDIINVOKER_MAPDATASOURCE_0_WHILE_BINDING_1_TO_JNDI_CONTEXT,
          new Object[] {"NamingException", jndiName});
    }
    catch (DataSourceCreateException dsce) {
      if (writer.infoEnabled()) writer.info(
          LocalizedStrings.JNDIInvoker_JNDIINVOKER_MAPDATASOURCE_0_WHILE_BINDING_1_TO_JNDI_CONTEXT,
          new Object[] {"DataSourceCreateException", jndiName});
    }
  }

  /**
   * @return Context the existing JNDI Context. If there is no pre-esisting JNDI
   *         Context, the GemFire JNDI Context is returned.
   */
  public static Context getJNDIContext() {
    return ctx;
  }

  /**
   * returns the GemFire TransactionManager.
   * 
   * @return TransactionManager
   */
  public static TransactionManager getTransactionManager() {
    return transactionManager;
  }
  //try to find websphere lookups since we came here
  /*
   * private static void print(String str) { if (DEBUG) {
   * System.err.println(str); } }
   */
}
